#!/usr/bin/env python

import asyncore, collections, itertools, os, pdb, select, socket, subprocess, \
    sys, time, urllib, urlparse
import gio, glib, gtk
import lanshare

def stock_image(stock_id):
    image = gtk.Image()
    image.set_from_stock(stock_id, gtk.ICON_SIZE_BUTTON)
    return image

def create_url(protocol, hostname, port, username, password, family):
    netloc = ''
    if username is not None:
        netloc += urllib.quote(username)
    if password is not None:
        netloc += ':' + urllib.quote(password)
    if username is not None or password is not None:
        netloc += '@'
    if family == socket.AF_INET6:
        # strip zone id
        netloc += '[%s]:%i' % (hostname.rsplit('%', 1)[0], port)
    else:
        if hostname == '0.0.0.0':
            hostname = 'localhost'
        netloc += '%s:%i' % (hostname, port)
    return urlparse.urlunparse(["ftp", netloc, "", "", "", ""])

def spawn_url_browser(url=None, url_parts=None):
    """Open the given peer URL with the most natural file manager for the
    current platform that we can find"""

    if url is None:
        url = create_url(*url_parts)
        url_without_password = create_url(
            *(url_parts[0:4] + [None] + url_parts[5:]))
    else:
        url_without_password = url
    print url
    print url_without_password

    if os.name == "nt":
        # this is how it's done programmatically? except that it won't invoke
        # the default file manager (explorer) on winders
        #ShellExecute(None, "explore", url, None, None, SW_SHOWNORMAL)

        # invoke explorer directly. why does it return 1 on success?
        if subprocess.call(["explorer", url]) == 1:
            return

    # explicitly invoking the file manager works as intended, as with the
    # windows explorer
    try:
        subprocess.check_call(["nautilus", url_without_password])
    except subprocess.CalledProcessError as exc:
        print exc
    else:
        return

    # this goes through to gnome-open on gnome, neither of which automatically
    # mount it, and complain that the address isn't available
    try:
        subprocess.check_call(["xdg-open", url])
    except subprocess.CalledProcessError as exc:
        print exc
    else:
        return

    # fall back to using the preferred browser
    import webbrowser
    webbrowser.open(url)

class PeerStore(gtk.GenericTreeModel):

    Key = collections.namedtuple("PeerStoreKey", ("hostname", "host", "serv", "scheme"))

    def __init__(self, data):
        gtk.GenericTreeModel.__init__(self)
        self.__data = data

    def on_get_n_columns(self):
        return 2

    def on_get_column_type(self, index):
        return str

    def on_get_flags(self):
        return gtk.TREE_MODEL_LIST_ONLY

    def on_get_iter(self, path):
        assert len(path) == 1
        ordered = sorted(self.__data.keys())
        try:
            return ordered[path[0]]
        except IndexError:
            pass

    def on_get_value(self, iter, column):
        if column == 0:
            return iter[0]
        elif column == 1:
            return create_url(iter[0], iter[1], iter[2], None, None, socket.AF_INET)

    def on_get_path(self, iter):
        return (sorted(self.__data.keys()).index(iter),)

    def on_iter_next(self, iter):
        ordered = sorted(self.__data.keys())
        index = ordered.index(iter)
        try:
            return ordered[index + 1]
        except IndexError:
            return None

    def on_iter_nth_child(self, parent, n):
        assert parent is None
        return sorted(self.__data.keys())[n]

class LanshareGtk(lanshare.Lanshare):
    #__slots__ = "peer_store",

    # remove peer if announce isn't received for this long
    peer_timeout = 5

    def create_gui(self, peer_data):

        ## peer page ##

        peer_store = PeerStore(peer_data)

        peer_view = gtk.TreeView(peer_store)
        peer_view.insert_column_with_attributes(
            -1, "Peer Name", gtk.CellRendererText(), text=0)
        peer_view.insert_column_with_attributes(
             -1, "Network Address", gtk.CellRendererText(), text=1)
        def callback(view, path, column):
            spawn_url_browser(view.get_model()[path][1])
        peer_view.connect("row-activated", callback)

        browse_peer_button = gtk.Button("Browse")
        browse_peer_button.set_image(stock_image(gtk.STOCK_HARDDISK))
        def callback(button, view):
            model, iter = view.get_selection().get_selected()
            spawn_url_browser(model[iter][1])
        browse_peer_button.connect("clicked", callback, peer_view)

        peer_buttons = gtk.VButtonBox()
        peer_buttons.add(browse_peer_button)
        peer_buttons.set_layout(gtk.BUTTONBOX_START)

        peers_page = gtk.HBox()
        peers_page.pack_start(peer_view)
        peers_page.pack_start(peer_buttons, expand=False)

        ## share page ##

        # share name, public, local path, write
        share_store = gtk.ListStore(str, bool, str, bool)

        share_view = gtk.TreeView(share_store)

        share_name_cell = gtk.CellRendererText()
        share_name_cell.set_property("editable", True)
        def callback(cell, path, text):
            oldtext = share_store[path][0]
            with self.config as config:
                shares = config.shares
                shares[text] = shares[oldtext]
                del shares[oldtext]
                config.save()
        share_name_cell.connect("edited", callback)
        share_view.insert_column_with_attributes(
            -1, "Share Name", share_name_cell, text=0)

        public_cell = gtk.CellRendererToggle()
        public_cell.set_property("activatable", True)
        def callback(cell, path, store):
            config = self.read_config()
            config.shares[store[path][0]].public = not cell.get_active()
            config.save()
        public_cell.connect("toggled", callback, share_store)
        share_view.insert_column_with_attributes(
            -1, "Public", public_cell, active=1)

        local_path_cell = gtk.CellRendererText()
        local_path_cell.set_property("editable", True)
        def callback(cell, path, text, store):
            sharename = store[path][0]
            with self.config as config:
                config.shares[sharename].path = text
                config.save()
            print path, store.get_iter(path)
            store.row_changed(path, store.get_iter(path))
        local_path_cell.connect("edited", callback, share_store)
        share_view.insert_column_with_attributes(
            -1, "Local Path", local_path_cell, text=2)

        cell = gtk.CellRendererToggle()
        cell.set_property("activatable", True)
        def callback(cell, path, store):
            config = self.read_config()
            share = config.shares[store[path][0]]
            share.perms = "".join(set(share.perms) | set("adfmw"))
            config.save()
        cell.connect("toggled", callback, share_store)
        share_view.insert_column_with_attributes(
            -1, "Write", cell, active=3)

        #share_view.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
        #def callback(view, path, column):
        #    self.browse_peer_by_url("file://" + view.get_model()[path][1])
        #shareview.connect("select-cursor-row", callback)

        share_buttons = gtk.VButtonBox()
        share_buttons.set_layout(gtk.BUTTONBOX_START)

        add_share_button = gtk.Button("New Share")
        add_share_button.connect("clicked", lambda button: self.add_share())
        add_share_button.set_image(stock_image(gtk.STOCK_NEW))
        share_buttons.add(add_share_button)

        remove_share_button = gtk.Button("Remove Share")
        remove_share_button.connect("clicked",
            lambda button: self.remove_share(share_view))
        remove_share_button.set_image(stock_image(gtk.STOCK_DELETE))
        share_buttons.add(remove_share_button)

        button = gtk.Button("Open Share")
        def callback(button, view):
            store, path_list = view.get_selection().get_selected_rows()
            for path in path_list:
                spawn_url_browser("file://" + os.path.abspath(store[path][2]))
        button.connect("clicked", callback, share_view)
        button.set_image(stock_image(gtk.STOCK_OPEN))
        share_buttons.add(button)

        button = gtk.Button("Set local path")
        def callback(button, view):
            store, iter = view.get_selection().get_selected()
            row = store[iter]
            dialog = gtk.FileChooserDialog(
                action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
                buttons=(
                    gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
                    gtk.STOCK_APPLY, gtk.RESPONSE_ACCEPT))
            if row[2] is not None:
                dialog.set_filename(row[2])
            if dialog.run() == gtk.RESPONSE_ACCEPT:
                config = self.read_config()
                config.shares[store[iter][0]].path = dialog.get_filename()
                config.save()
            dialog.destroy()
        button.connect("clicked", callback, share_view)
        button.set_image(stock_image(gtk.STOCK_FIND_AND_REPLACE))
        share_buttons.add(button)

        share_page = gtk.HBox()
        share_page.add(share_view)
        share_page.pack_start(share_buttons, expand=False)

        ## user page ##

        # username, password, shares
        user_store = gtk.ListStore(str, str, str)
        user_view = gtk.TreeView(user_store)
        username_cell = gtk.CellRendererText()
        username_cell.set_property("editable", True)
        def callback(cell, path, text, store):
            config = self.read_config()
            oldtext = store[path][0]
            config.users[text] = config.users[oldtext]
            del config.users[oldtext]
            config.save()
        username_cell.connect("edited", callback, user_store)
        user_view.insert_column_with_attributes(
            -1, "Username", username_cell, text=0)
        passwordcell = gtk.CellRendererText()
        passwordcell.set_property("editable", True)
        def callback(cell, path, text):
            username = user_store[path][0]
            with self.config as config:
                config.users[username].password = text
                config.save()
        passwordcell.connect("edited", callback)
        user_view.insert_column_with_attributes(
            -1, "Password", passwordcell, text=1)
        shares_cell = gtk.CellRendererText()
        def callback(column, cell, model, iter):
            username = model[iter][0]
            with self.config as config:
                cell.set_property("text", config.users[username].granted)
        user_view.insert_column_with_data_func(
            -1, "Granted Shares", shares_cell, callback)
        def callback(view, path, column):
            row = view.get_model()[path]
            username, password = row[0], row[1]
            if password is None:
                password = 'lanshare@localhost'
            hostname, port = self.server.getsockname()[0:2]
            spawn_url_browser(url_parts=["ftp", hostname, port, username, password, self.server.family])
        user_view.connect("row-activated", callback)

        add_user_button = gtk.Button("New User", stock=gtk.STOCK_ADD)
        def callback(button):
            self
        add_user_button.connect("clicked", lambda button: self.add_user())

        remove_user_button = gtk.Button("Remove User", stock=gtk.STOCK_REMOVE)
        def callback(button, view):
            model, iter = view.get_selection().get_selected()
            username = model[iter][0]
            config = self.read_config()
            del config.users[username]
            config.save()
        remove_user_button.connect("clicked", callback, user_view)

        test_user_button = gtk.Button("Test", stock=gtk.STOCK_CONNECT)
        def callback(button, view):
            store, iter = view.get_selection().get_selected()
            row = store[iter]
            host, serv = self.server.getsockname()[0:2]
            username, password = row[0], row[1]
            spawn_url_browser(url_parts=["ftp", host, serv, username, password, self.server.family])
        test_user_button.connect("clicked", callback, user_view)

        user_buttons = gtk.VButtonBox()
        user_buttons.add(add_user_button)
        user_buttons.add(remove_user_button)
        user_buttons.add(test_user_button)
        user_buttons.set_layout(gtk.BUTTONBOX_START)

        user_page = gtk.HBox()
        user_page.add(user_view)
        user_page.pack_start(user_buttons, expand=False)

        ## dialog ##

        notebook = gtk.Notebook()
        notebook.append_page(peers_page)
        notebook.set_tab_label_text(peers_page, "Peers")
        notebook.append_page(share_page)
        notebook.set_tab_label_text(share_page, "Shares")
        notebook.append_page(user_page)
        notebook.set_tab_label_text(user_page, "Users")

        window = gtk.Window(gtk.WINDOW_TOPLEVEL)
        window.connect("destroy", self.destroy)
        window.add(notebook)
        window.set_title(lanshare.__title__)
        window.show_all()

        # save these for later manipulation
        self.peer_store = peer_store
        self.share_store = share_store
        self.user_store = user_store
        self.user_view = user_view

    def create_announce_listener(self, peer_data):
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.bind(self.address)
        glib.io_add_watch(sock, glib.IO_IN, self.announce_listener_event, peer_data)

    def announce_listener_event(self, source, cond, peer_data):
        data, addr = source.recvfrom(512)
        recvtime = time.time()
        hostname, port, protocol = eval(data)
        key = PeerStore.Key(hostname, addr[0], port, protocol)
        #print "received", key
        changed = key in peer_data
        peer_data[key] = recvtime
        iter = self.peer_store.create_tree_iter(key)
        path = self.peer_store.get_path(iter)
        if changed:
            self.peer_store.row_changed(path, iter)
        else:
            self.peer_store.row_inserted(path, iter)
        #self.peer_store.clear()
        return True

    def user_shares_share_name_cell_data_func(
            self, column, cell, model, iter, share):
        cell.set_property("text", share)

    def user_shares_share_allowed_cell_data_func(
            self, column, cell, model, iter, share):
        #print "public cdf"
        user = model[iter][0]
        config = self.read_config()
        active = share in config.shares and config.shares[share].public or user in config.users and share in config.users[user].granted
        cell.set_active(active)

    def user_shares_share_allowed_toggled(self, cell, path, sharename):
        user = self.user_store[path][0]
        config = self.read_config()
        allowed = config.users[user].granted
        if sharename in allowed:
            allowed.remove(sharename)
        else:
            allowed.append(sharename)
        config.users[user].granted = allowed
        config.save()

    def server_config_changed(self):
        self.share_store.clear()
        self.user_store.clear()
        config = self.read_config()
        column = self.user_view.get_column(2)
        column.clear()
        for name in config.shares:
            share = config.share(name)
            row = [name, share.public, share.path,
                set(share.perms).issuperset("adfmw")]
            self.share_store.append(row)
            cell = gtk.CellRendererToggle()
            cell.set_property("activatable", not share.public)
            cell.connect(
                "toggled",
                self.user_shares_share_allowed_toggled,
                name)
            column.pack_start(cell, expand=False)
            column.set_cell_data_func(
                cell, self.user_shares_share_allowed_cell_data_func, name)
            cell = gtk.CellRendererText()
            column.pack_start(cell, expand=False)
            column.set_cell_data_func(
                cell, self.user_shares_share_name_cell_data_func, name)
        for name in config.users:
            user = config.users[name]
            self.user_store.append([name, user.password, user.granted])

    def __init__(self):
        lanshare.Lanshare.__init__(self)
        peer_data = {}
        self.create_gui(peer_data)
        self.create_announce_listener(peer_data)
        file = gio.File("shares.conf")
        monitor = file.monitor()
        monitor.set_rate_limit(100)
        def callback(monitor, file, other_file, event):
            self.server_config_changed()
        monitor.connect("changed", callback)
        file = gio.File("users.conf")
        monitor = file.monitor()
        monitor.set_rate_limit(100)
        def callback(monitor, file, other_file, event):
            self.server_config_changed()
        monitor.connect("changed", callback)
        self.server_config_changed()

    def peer_tick(self):
        try:
            self.send_peer_announce()
            from time import time
            delrefs = []
            for row in self.peerstore:
                if time() >= row[3] + self.peer_timeout:
                    delrefs.append(gtk.TreeRowReference(row.model, row.path))
            for ref in delrefs:
                del self.peerstore[ref.get_path()]
        except:
            import traceback
            traceback.print_exc()
        finally:
            return True

    def add_user(self):
        config = self.read_config()
        for num in itertools.count():
            name = "newuser%d" % num
            if name not in config.users:
                break
        config.users.setdefault(name)
        config.save()

    def add_share(self):
        config = self.read_config()
        for num in itertools.count():
            name = "newshare%d" % num
            if name not in config.shares:
                break
        config.shares.setdefault(name)
        config.save()

    def remove_share(self, view):
        store, rowpaths = view.get_selection().get_selected_rows()
        config = self.read_config()
        for path in rowpaths:
            del config.shares[store[path][0]]
        config.save()

    def destroy(self, widget=None):
        print "Main window destroyed"
        self.server.close_all()
        gtk.main_quit()

    def idle_callback(self):
        try:
            # long enough for a windows jiffy, if there is more than count 1 worth
            # of stuff to serve, we'll return early and the idle handler will throw
            # us straight back in
            self.server.serve_forever(timeout=0.01, count=1)
        except KeyboardInterrupt:
            self.destroy()
            return False
        return True

    def __call__(self):
        glib.idle_add(self.idle_callback)
        gtk.main()

def main():
    import logging
    logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
    gtk.settings_get_default().set_long_property("gtk-button-images", True, "lanshare main")
    LanshareGtk()()

if __name__ == "__main__":
    main()
